package jmine.tec.web.wicket.cronExpression;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jmine.tec.utils.date.MonthEnum;
import jmine.tec.utils.date.TimeUnitEnum;
import jmine.tec.utils.date.WeekDaysEnum;

import org.apache.commons.lang.StringUtils;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.ListMultipleChoice;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.model.util.ListModel;
import org.quartz.CronExpression;

/**
 * Abstract class that implements {@link CronExpressionTypeStrategy}. Contais common methods between the strategies.
 * 
 * @author Fabio Sakiyama
 * @created 24/11/2011
 */
public abstract class AbstractCronExpressionTypeStrategy extends Panel implements CronExpressionTypeStrategy {

    private static final int WEEK_DAY_LENGTH = 3;

    protected static final int MINUTE_POSITION = 1;

    protected static final int HOUR_POSITION = 2;

    protected static final int DAYS_POSITION = 3;

    protected static final int MONTHS_POSITION = 4;

    protected static final int WEEK_DAYS_POSITION = 5;

    protected static final int DAY_HOURS = 24;

    protected static final int MINUTE_IN_SECONDS = 60;

    protected static final int MAX_DAYS_IN_MONTH = 31;

    protected static final int NUMBER_MONTHS = 12;

    protected static final int NUMBER_WEEK_DAYS = 7;

    protected static final int MAX_HOUR = 23;

    protected static final int MIN_HOUR = 1;

    protected static final int MAX_MINUTES = 59;

    protected static final int MIN_MINUTES = 1;

    private IModel<String> model;

    private Map<String, String> numberMonthMap;

    private Map<String, WeekDaysEnum> numberWeekDayMap;

    private boolean specified;

    /**
     * C'tor
     * 
     * @param id id
     */
    public AbstractCronExpressionTypeStrategy(String id) {
        super(id);
        this.initializeNumberMonthMap();
        this.initializeNumberWeekDayMap();
    }

    /**
     * Initializes the numberMonthMap with keys being the month's numbers and values being the month's names.
     */
    private void initializeNumberMonthMap() {
        this.numberMonthMap = new HashMap<String, String>();
        this.numberMonthMap.put("1", "JANUARY");
        this.numberMonthMap.put("2", "FEBRUARY");
        this.numberMonthMap.put("3", "MARCH");
        this.numberMonthMap.put("4", "APRIL");
        this.numberMonthMap.put("5", "MAY");
        this.numberMonthMap.put("6", "JUNE");
        this.numberMonthMap.put("7", "JULY");
        this.numberMonthMap.put("8", "AUGUST");
        this.numberMonthMap.put("9", "SEPTEMBER");
        this.numberMonthMap.put("10", "OCTOBER");
        this.numberMonthMap.put("11", "NOVEMBER");
        this.numberMonthMap.put("12", "DECEMBER");
    }

    /**
     * Initializes the numberMonthMap with keys being the week days numbers and values being the week days names.
     */
    private void initializeNumberWeekDayMap() {
        this.numberWeekDayMap = new HashMap<String, WeekDaysEnum>();
        this.numberWeekDayMap.put("SUN", WeekDaysEnum.SUNDAY);
        this.numberWeekDayMap.put("MON", WeekDaysEnum.MONDAY);
        this.numberWeekDayMap.put("TUE", WeekDaysEnum.TUESDAY);
        this.numberWeekDayMap.put("WED", WeekDaysEnum.WEDNESDAY);
        this.numberWeekDayMap.put("THU", WeekDaysEnum.THURSDAY);
        this.numberWeekDayMap.put("FRI", WeekDaysEnum.FRIDAY);
        this.numberWeekDayMap.put("SAT", WeekDaysEnum.SATURDAY);
    }

    /**
     * Returns the month's name by it's number. Examples: 1 - JANUARY, 10 - OCTOBER
     * 
     * @param number number of the month
     * @return name of the month
     */
    protected String getMonthNameByNumber(String number) {
        return this.numberMonthMap.get(number);
    }

    /**
     * Returns the week day name by it's initial (first three characters). Examples: SUN - SUNDAY, MON - MONDAY
     * 
     * @param initial initial of the week day name
     * @return name of the week day name
     */
    protected WeekDaysEnum getWeekDayNameByWeekDayInitial(String initial) {
        return this.numberWeekDayMap.get(initial);
    }

    /**
     * Returns the first three characters of a week day name. Examples: SUNDAY - SUN, MONDAY - MON
     * 
     * @param weekDayName name of the week day
     * @return first three characters of the week day name
     */
    protected String getWeekDayInitialByWeekDayName(String weekDayName) {
        return StringUtils.left(weekDayName, WEEK_DAY_LENGTH);
    }

    /**
     * Receives the initial time and the end time, and formats it to occur in the given interval.
     * 
     * @param initial time
     * @param end time
     * @return formatted interval part of the cron expression.
     */
    protected String formatInterval(String initial, String end) {
        return initial.concat("-" + end + " ");
    }

    /**
     * Formats a number to be the periodicity of a cron expression.
     * 
     * @param periodicity that will occur.
     * @return formatted periodicity part of the cron expression.
     */
    protected String formatPeriodicity(Integer periodicity) {
        return "*/".concat(periodicity.toString()) + " ";
    }

    /**
     * Return a list with the max number of days in a month.
     * 
     * @return list with the max number of days in a month.
     */
    protected Collection<? extends String> maxDaysMonth() {
        List<String> list = new ArrayList<String>();
        for (int i = 1; i <= MAX_DAYS_IN_MONTH; i++) {
            list.add(String.valueOf(i));
        }
        return list;
    }

    /**
     * Method that formats the cron expression fields that represent the days, months and weekdays. If the selectedDays, selectedWeekDays or
     * the selectedMonths are nulls, it will be '*' or '?'.
     * 
     * @param selectedDays to trigger the cron expression.
     * @param selectedMonths to trigger the cron expression.
     * @param selectedWeekDays to trigger the cron expression.
     * @return formatted days, months and weekdays fields of the cron expression.
     */
    protected String formatDaysMonthsWeekDays(List<String> selectedDays, List<MonthEnum> selectedMonths, List<WeekDaysEnum> selectedWeekDays) {
        String daysMonthsWeekDays = "";
        Collections.sort(selectedDays);
        Collections.sort(selectedMonths);
        Collections.sort(selectedWeekDays);
        if (selectedDays.size() == MAX_DAYS_IN_MONTH || selectedDays.isEmpty()) {
            daysMonthsWeekDays = daysMonthsWeekDays.concat("?");
        } else {
            for (String day : selectedDays) {
                daysMonthsWeekDays = daysMonthsWeekDays.concat(day + ",");
            }
            daysMonthsWeekDays = StringUtils.removeEnd(daysMonthsWeekDays, ",");
            this.setSpecified(true);
        }
        daysMonthsWeekDays = daysMonthsWeekDays.concat(" ");
        if (selectedMonths.size() == NUMBER_MONTHS || selectedMonths.isEmpty()) {
            daysMonthsWeekDays = daysMonthsWeekDays.concat("*");
        } else {
            for (MonthEnum month : selectedMonths) {
                daysMonthsWeekDays = daysMonthsWeekDays.concat(month.ordinal() + ",");
            }
            daysMonthsWeekDays = StringUtils.removeEnd(daysMonthsWeekDays, ",");
        }
        daysMonthsWeekDays = daysMonthsWeekDays.concat(" ");
        if (selectedWeekDays.size() == NUMBER_WEEK_DAYS || selectedWeekDays.isEmpty()) {
            if (this.isSpecified()) {
                daysMonthsWeekDays = daysMonthsWeekDays.concat("?");
            } else {
                daysMonthsWeekDays = daysMonthsWeekDays.concat("*");
            }
        } else {
            for (WeekDaysEnum weekDay : selectedWeekDays) {
                daysMonthsWeekDays = daysMonthsWeekDays.concat(this.getWeekDayInitialByWeekDayName(weekDay.name()) + ",");
            }
            daysMonthsWeekDays = StringUtils.removeEnd(daysMonthsWeekDays, ",");
        }
        daysMonthsWeekDays = daysMonthsWeekDays.concat(" ");
        return daysMonthsWeekDays;
    }

    /**
     * Method called everytime a field is changed. It calls the nullCheckAllFields method, check if the cron is valid, and then set
     * {@link IModel} object.
     */
    public final void buildCronExpression() {
        String cron = null;
        if (this.nullCheckAllFields()) {
            cron = this.doBuildCronExpression();
            if (!CronExpression.isValidExpression(cron)) {
                this.error("The cron expression [" + cron + "] is invalid! Check Quartz documentation.");
                return;
            }
            this.model.setObject(cron);
        }
    }

    /**
     * Abstract method that build the cron expression.
     * 
     * @return formatted cron expression.
     */
    protected abstract String doBuildCronExpression();

    /**
     * Create a {@link ListMultipleChoice} with the maximum days of a month.
     * 
     * @param id wicket id of the {@link ListMultipleChoice}
     * @return {@link ListMultipleChoice} with the maximum days of a month.
     */
    protected ListMultipleChoice<String> createDaysListMultipleChoice(String id) {
        List<String> list = new ArrayList<String>();
        for (int i = 1; i <= MAX_DAYS_IN_MONTH; i++) {
            list.add(String.valueOf(i));
        }
        ListModel<String> listModel = new ListModel<String>(list);
        ListMultipleChoice<String> daysListMultipleChoice = new ListMultipleChoice<String>(id, listModel);
        daysListMultipleChoice.setDefaultModel(new PropertyModel<ListMultipleChoice<String>>(this, "daysList"));
        daysListMultipleChoice.setOutputMarkupId(true);
        return daysListMultipleChoice;
    }

    /**
     * Create a {@link DropDownChoice} with minutes options
     * 
     * @param id wicket id of the {@link DropDownChoice}
     * @return {@link DropDownChoice} with minute options.
     */
    protected DropDownChoice<String> createMinutesDropDownChoice(String id) {
        List<String> minutes = new ArrayList<String>();
        for (int i = 0; i < MINUTE_IN_SECONDS; i++) {
            minutes.add(String.valueOf(i));
        }
        ListModel<String> listModel = new ListModel<String>(minutes);
        DropDownChoice<String> minutesDropDownChoice = new DropDownChoice<String>(id, listModel);
        minutesDropDownChoice.setDefaultModel(new PropertyModel<String>(this, id));
        minutesDropDownChoice.setOutputMarkupId(true);
        return minutesDropDownChoice;
    }

    /**
     * Create a {@link DropDownChoice} with hours options.
     * 
     * @param id wicket id of the {@link DropDownChoice}
     * @return {@link DropDownChoice} with hours options.
     */
    protected DropDownChoice<String> createHourDropDownChoice(String id) {
        List<String> hours = new ArrayList<String>();
        for (int i = 0; i < DAY_HOURS; i++) {
            hours.add(String.valueOf(i));
        }
        ListModel<String> listModel = new ListModel<String>(hours);
        DropDownChoice<String> hoursDropDownChoice = new DropDownChoice<String>(id, listModel);
        hoursDropDownChoice.setDefaultModel(new PropertyModel<String>(this, id));
        hoursDropDownChoice.setOutputMarkupId(true);
        return hoursDropDownChoice;
    }

    /**
     * Create a {@link ListMultipleChoice} with the days of a week ( {@link WeekDaysEnum} ).
     * 
     * @param id wicket id of the {@link ListMultipleChoice}
     * @return {@link ListMultipleChoice} with the days of a week ( {@link WeekDaysEnum} ).
     */
    protected ListMultipleChoice<WeekDaysEnum> createWeekDaysListMultipleChoice(String id) {
        List<WeekDaysEnum> list = new ArrayList<WeekDaysEnum>();
        list.addAll(Arrays.asList(WeekDaysEnum.values()));
        ListModel<WeekDaysEnum> listModel = new ListModel<WeekDaysEnum>(list);
        ListMultipleChoice<WeekDaysEnum> weekDaysListMultipleChoice = new ListMultipleChoice<WeekDaysEnum>(id, listModel);
        weekDaysListMultipleChoice.setDefaultModel(new PropertyModel<WeekDaysEnum>(this, "weekDaysList"));
        weekDaysListMultipleChoice.setOutputMarkupId(true);
        return weekDaysListMultipleChoice;
    }

    /**
     * Create a {@link ListMultipleChoice} with the months ( {@link MonthEnum} ).
     * 
     * @param id wicket id of the {@link ListMultipleChoice}
     * @return {@link ListMultipleChoice} with the months ( {@link MonthEnum} ).
     */
    protected ListMultipleChoice<MonthEnum> createMonthsListMultipleChoice(String id) {
        List<MonthEnum> list = new ArrayList<MonthEnum>();
        list.addAll(Arrays.asList(MonthEnum.values()));
        ListModel<MonthEnum> listModel = new ListModel<MonthEnum>(list);
        ListMultipleChoice<MonthEnum> monthsListMultipleChoice = new ListMultipleChoice<MonthEnum>(id, listModel);
        monthsListMultipleChoice.setDefaultModel(new PropertyModel<MonthEnum>(this, "monthsList"));
        monthsListMultipleChoice.setOutputMarkupId(true);
        return monthsListMultipleChoice;
    }

    /**
     * Create a {@link TextField} referent a periodicity of a cron expression.
     * 
     * @param id wicket id of {@link TextField}
     * @return {@link TextField} referent a periodicity of a cron expression.
     */
    protected TextField<Integer> createPeriodicityTextField(String id) {
        TextField<Integer> periodicityTextField = new TextField<Integer>(id, Integer.class);
        periodicityTextField.setDefaultModel(new PropertyModel<Integer>(this, id));
        periodicityTextField.setOutputMarkupId(true);
        return periodicityTextField;
    }

    /**
     * Create a {@link DropDownChoice} with the options of a periodicity, minutes or hour.
     * 
     * @param id wicket id of the {@link DropDownChoice}.
     * @return {@link DropDownChoice} with the options of a periodicity, minutes or hour.
     */
    protected DropDownChoice<TimeUnitEnum> createPeriodicityTypes(String id) {
        List<TimeUnitEnum> periodicityListType = new ArrayList<TimeUnitEnum>();
        periodicityListType.add(TimeUnitEnum.HOURS);
        periodicityListType.add(TimeUnitEnum.MINUTES);
        ListModel<TimeUnitEnum> listModel = new ListModel<TimeUnitEnum>(periodicityListType);
        DropDownChoice<TimeUnitEnum> periodicityDropDownChoice = new DropDownChoice<TimeUnitEnum>(id, listModel);
        periodicityDropDownChoice.setDefaultModel(new PropertyModel<ListMultipleChoice<TimeUnitEnum>>(this, id));
        periodicityDropDownChoice.setOutputMarkupId(true);
        return periodicityDropDownChoice;
    }

    /**
     * @return the iModel
     */
    public IModel<String> getiModel() {
        return this.model;
    }

    /**
     * @param iModelParameter the iModel to set
     */
    public void setiModel(IModel<String> iModelParameter) {
        this.model = iModelParameter;
    }

    /**
     * @return the isSpecified
     */
    public boolean isSpecified() {
        return this.specified;
    }

    /**
     * @param specified the isSpecified to set
     */
    public void setSpecified(boolean specified) {
        this.specified = specified;
    }

}
